﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MLSharp.See5
{
	/// <summary>
	/// Aggregates options that can be passed to the underlying
	/// See5 executable used by <see cref="See5"/>.
	/// </summary>
	public class See5Options
	{
		#region Private Fields

		/// <summary>
		/// Contains the misclassification costs.
		/// </summary>
		private IList<MisclassificationCost> mMisclassificationCosts = new List<MisclassificationCost>();

		#endregion

		#region Public Properties

		/// <summary>
		/// Enables or disables Subsets.
		/// </summary>
		/// <value>The default is false (disabled).</value>
		/// <remarks>
		/// If set to true, branches on discrete values will be based on
		/// a subset of possible values instead of on an individual value.
		/// See http://www.rulequest.com/see5-win.html#SUBSETS.
		/// </remarks>
		public bool Subset { get; set; }

		/// <summary>
		/// Enables or disables Boosting.
		/// </summary>
		/// <value>The default is false (disabled).</value>
		/// <remarks>
		/// If set to true, boosting will be performed up to
		/// <see cref="BoostingTrials"/> trials. 
		/// See http://www.rulequest.com/see5-win.html#BOOSTING.
		/// </remarks>
		public bool Boosting { get; set; }

		/// <summary>
		/// Gets or sets the number of boosting trials to perform.
		/// </summary>
		/// <value>The default is 10.</value>
		/// <remarks>
		/// If <see cref="Boosting"/> is set to true, this controls
		/// how many trials to perform, otherwise this value is
		/// ignored. 
		/// See http://www.rulequest.com/see5-win.html#BOOSTING.
		/// </remarks>
		public int BoostingTrials { get; set; }

		/// <summary>
		/// Enables or disables Winnowing.
		/// </summary>
		/// <value>The default is false (disabled).</value>
		/// <remarks>
		/// If set to true, See5 will "winnow" the set of available attributes,
		/// using only the useful ones.  This option is only useful when
		/// you have a very high number of attributes.
		/// See http://www.rulequest.com/see5-win.html#WINNOWING.
		/// </remarks>
		public bool WinnowAttributes { get; set; }

		/// <summary>
		/// Enables or disables global pruning.
		/// </summary>
		/// <value>The default is true (enabled).</value>
		/// <remarks>
		/// See5 performs two passes to prune decision tree.  The second
		/// pass looks at the entire tree and is called Global Pruning.
		/// Disabling this option usually results in larger trees.
		/// See http://www.rulequest.com/see5-win.html#OTHER.
		/// </remarks>
		public bool GlobalPruning { get; set; }

		/// <summary>
		/// Gets or sets the Pruning CF level (from 0 to 100).
		/// </summary>
		/// <value>The default is 25.</value>
		/// <remarks>
		/// Pruning CF affects how See5 calculates error rates.  Adjusting
		/// this changes how See5 prunes.  Small values cause more of the tree
		/// to be pruned while large values cause less pruning.
		/// See http://www.rulequest.com/see5-win.html#OTHER.
		/// </remarks>
		public int PruningCF { get; set; }

		/// <summary>
		/// Gets or sets the Minimum Cases.
		/// </summary>
		/// <value>The default is 2.</value>
		/// <remarks>
		/// This option controls the degree to which the initial tree
		/// can fit the data set.  Values higher than the default (2) can
		/// lead to a looser fit.
		/// See http://www.rulequest.com/see5-win.html#OTHER.
		/// </remarks>
		public int MinimumCases { get; set; }

		/// <summary>
		/// Enables or disables Fuzzy Thresholds.
		/// </summary>
		/// <value>The default is false (disabled).</value>
		/// <remarks>
		/// If enabled, See5 will soften thresholds within the tree. 
		/// By default (disabled), See5 will generate sharp thresholds,
		/// so different paths may be taken for 6.0 and 6.01 even though
		/// the two values are very close.
		/// See http://www.rulequest.com/see5-win.html#FUZZY
		/// </remarks>
		public bool FuzzyThresholds { get; set; }

		/// <summary>
		/// Gets or sets the indexes of any attributes to be ignored by See5.
		/// </summary>
		/// <value>The default is an empty array (meaning all attributes will be used).</value>
		/// <remarks>
		/// You can tell See5 to ignore zero or more attributes.  
		/// </remarks>
		public int[] IgnoredAttributeIndexes { get; set;}

		/// <summary>
		/// The misclassification costs.
		/// </summary>
		/// <remarks>
		/// This property is *NOT* supported yet.
		/// </remarks>
		public IList<MisclassificationCost> MisclassificationCosts
		{
			get { return mMisclassificationCosts; }
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates an instance with all properties
		/// set to their default values.
		/// </summary>
		public See5Options()
		{
			this.BoostingTrials = 10;
			this.GlobalPruning = true;
			this.PruningCF = 25;
			this.MinimumCases = 2;
			this.IgnoredAttributeIndexes = new int[0];
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Converts the current options to a string that 
		/// can be passed to the See5 command line executable.
		/// </summary>
		/// <returns>A string with a leading whitespace that contains
		/// command line arguments that can be passed to See5.  If
		/// everything is set to the default value, an empty
		/// string is returned.</returns>
		public string ToCommandString()
		{
			List<string> arguments = new List<string>();

			if (this.Subset)
			{
				arguments.Add("-s");
			}

			if (this.Boosting)
			{
				arguments.Add("-t");

				if (this.BoostingTrials <= 0)
				{
					throw (new InvalidOperationException("BoostingTrials must be a positive integer."));
				}

				arguments.Add(this.BoostingTrials.ToString());
			}

			if (this.WinnowAttributes)
			{
				arguments.Add("-w");
			}

			if (!this.GlobalPruning)
			{
				arguments.Add("-g");
			}

			if (this.PruningCF != 25)
			{
				if (this.PruningCF <= 0 || this.PruningCF >= 100)
				{
					throw (new InvalidOperationException("PruningCF must be an integer greater than 0 and less than 100."));
				}

				arguments.Add("-c");
				arguments.Add(this.PruningCF.ToString());
			}

			if (this.MinimumCases != 2)
			{
				if (this.MinimumCases <= 0)
				{
					throw (new InvalidOperationException("MinimumCases must be an integer greater than zero."));
				}

				arguments.Add("-m");
				arguments.Add(this.MinimumCases.ToString());
			}

			if (this.FuzzyThresholds)
			{
				arguments.Add("-p");
			}

			if (arguments.Count > 0)
			{
				return " " + string.Join(" ", arguments.ToArray());
			}
			else
			{
				return string.Empty;
			}
		}

		#endregion
	}
}
